feat: Dashboard groups with tabs, collapsible/bordered options, and panel organization#1972
feat: Dashboard groups with tabs, collapsible/bordered options, and panel organization#1972alex-fedotyev wants to merge 8 commits intomainfrom
Conversation
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Knip - Unused Code Analysis⚪ No changes detected (231 issues on both main and PR) What is this?Knip finds unused files, dependencies, and exports in your codebase. Run |
PR Review
|
E2E Test Results✅ All tests passed • 92 passed • 3 skipped • 1012s
Tests ran across 4 shards in parallel. |
9d1a5c2 to
0d2b11f
Compare
a7afc78 to
fd8799e
Compare
fd8799e to
20a628e
Compare
d65d046 to
0fb0522
Compare
0fb0522 to
e38c98b
Compare
e38c98b to
74ee92a
Compare
74ee92a to
4aee30f
Compare
4aee30f to
ae5981a
Compare
ae5981a to
82579ce
Compare
d7cd008 to
24f3b5f
Compare
24f3b5f to
d1c641a
Compare
1b87b14 to
ca82900
Compare
Add 'group' type alongside 'section'. Groups always have tabs array (min 1 tab). 1 tab = plain group, 2+ = tab bar. Tiles get optional tabId for tab assignment. activeTabId persisted like collapsed state (Grafana/Kibana shared view state pattern).
DashboardDndContext (116 lines): sortable container reorder provider. DashboardDndComponents (98 lines): EmptyContainerPlaceholder with full-width [+ Add] button, SortableSectionWrapper.
1 tab = plain header (group identity IS the first tab). 2+ tabs = tab bar with hover-only x, inline +, double-click rename. stopPropagation on rename inputs to prevent Cmd+Left bubbling. Render-prop children(activeTabId). Theme-aware borders.
Grip icon on hover. stopPropagation on rename input keyboard events. Theme-aware var(--mantine-color-default-border).
useDashboardContainers (280 lines): section/group CRUD, tab management. Groups created with 1 tab. Add Tab creates 2nd. Delete to 1 keeps tab. Header rename syncs tabs[0]. Always-confirm delete. handleTabChange intentionally persists activeTabId (documented). useTileSelection (76 lines): Shift+click + Cmd+G grouping. Clears tabId on grouped tiles to prevent orphaned tab assignments.
Fill-right-then-wrap tile positioning via calculateNextTilePosition (w=8, h=10 matching original defaults). Floating drag bar on hover (no content shift). Move dropdown: tab targets shown as pipe-separated siblings with target in normal weight, others dimmed (Logs | Metrics). Section empty state wired with onAddTile. Cross-container moves via dropdown. Delete confirmation always shown. Select-and-group Cmd+G. makeId uses larger ID space to reduce collision probability.
ca82900 to
c363674
Compare
54 tests: group always has 1 tab, add tab creates 2nd, title syncs from tabs[0], delete to 1 keeps tab, fill-right positioning, tabId cleanup on grouping, section reorder, tile grouping.
c363674 to
62f8b3d
Compare
Temporary — remove before merge.
|
Hey, sorry not going to get to this today. I'll follow up after the weekend |
|
@alex-fedotyev one thing that wasn't clear to me when checking out is what the main difference between groups and sections are. I understand sections can collapse, and groups can't. But is there a reason why we don't have collapsible groups (and then no longer need sections?) |
@MikeShi42 - Great question — I've been thinking through this and I see two real options: Option A: Two types, better namesRename "Section" → "Row". Keep "Group" as the bordered container with optional tabs.
Benefits:
Tradeoff: two concepts to learn, but the names make the distinction obvious. Option B: One type, always borderedDrop section/row entirely. Just "Group" — always bordered, always collapsible, optionally tabbed. Benefits:
Tradeoff: every organizational element is a bordered box, which makes dashboards visually heavier. Users who just want a collapsible label to divide their dashboard into logical areas (the most common organizational pattern in dashboarding tools) are forced into bordered containers. Variable-driven repeat would need to handle both row-style duplication and tab-style generation within a single type, making the automation model more complex. Conditional tabs that filter out all visible tabs leave an empty bordered box — either shown (looks broken) or hidden (implicit disappearance is confusing). Stretch: A My recommendation: Option AThe real issue isn't that there are two types — it's that "section" and "group" sound like synonyms. Renaming section to "row" makes the distinction self-evident. And keeping a lightweight option matters — not every dashboard needs boxes, but almost every large dashboard needs collapsible dividers. |
…ered options (#2015) ## Summary Merges the separate "section" and "group" container types into a single **DashboardContainer** concept with configurable options: - **Collapsible** (optional, default true) — chevron toggle with URL-based collapse state - **Bordered** (optional, default true) — visual border around the container - **Tabbed** (optional) — add tabs to any container; tab bar appears with 2+ tabs. Active tab is per-viewer via URL state. - **Alert indicators** — red dot on tabs (multi-tab groups) and on the group header (plain/collapsed) when a tile is in `AlertState.ALERT`, so an alerting group is visible at a glance even when the alerting tile is below the fold This addresses the UX concern from #1972 that "section" and "group" are near-synonyms that force users to choose between two similar concepts. ### Screenshots **Dashboard with groups and tabs (expanded):**  **Overflow menu (Add Tab, Collapse, Border, Delete):**  **Collapsed with pipe-separated tab names (Overview | Latency | Errors):**  ### Commits (review guide) | Area | What to review | |------|----------------| | **Schema** — `types.ts` | new optional fields (`tabs`, `collapsible`, `bordered`, `activeTabId`, `tabId`); `type` discriminator removed | | **DnD infrastructure** | `DashboardDndContext.tsx`, `DashboardDndComponents.tsx`: sortable wrappers, drag handle | | **`DashboardContainer`** | `DashboardContainer.tsx` + `GroupTabBar.tsx`: unified component with tabs, collapse, borders, alerts, a11y | | **Hooks** | `useDashboardContainers.tsx`: container/tab CRUD. `useTileSelection.ts`: multi-select + Cmd+G (auto-deletes emptied source containers) | | **Dashboard page** | `DBDashboardPage.tsx`: wiring, alert computation, DnD, tile positioning, URL-based active-tab + collapse state | | **Tests** | `DashboardContainer.test.tsx`, `useDashboardContainers.test.tsx`, `useTileSelection.test.tsx`, `dashboardSections.test.tsx` | ### Key decisions - **No `type` discriminator** — containers are defined by properties. Extensible via additional fields later (e.g., planned variable-repeat feature could add `repeat: { filterId; as: 'tabs' \| 'sections' }` cleanly onto the same container). - **Collapsed label** — pipe-separated tab names (max 4 + "…"), not dot-separated, no duplicate header. - **Alert indicators** — red dot on tabs with `AlertState.ALERT` tiles. Also shown at the group-header level when no tab bar is rendered (plain/collapsed), so a single alerting tile in a large dashboard isn't hidden. - **Accessibility** — chevron has role/tabIndex/aria-expanded/onKeyDown. Hidden controls removed from tab order. - **Per-viewer UI state via URL** — collapse/expand and active-tab are per-viewer (URL), falling back to DB defaults. Switching tabs in your browser doesn't affect another viewer's session. - **Tab delete confirmation** — deleting a tab with tiles opens a 3-button modal: Cancel / Move Tiles to [first remaining tab] / Delete Tab & Tiles. Destructive option is the red primary. - **Group delete confirmation** — deleting a group with tiles opens a 3-button modal: Cancel / Ungroup Tiles / Delete Group & Tiles. Ungroup preserves tiles as top-level; Delete removes them. Works consistently for plain groups, legacy sections, and multi-tab groups (all tiles across all tabs are handled). ### Backward compatibility - Dashboards without containers render as before - Old `type: 'section'` containers parse successfully (Zod strips extra field) - New optional fields (`collapsible`, `bordered`, `tabs`, `activeTabId`, `tabId`) — `undefined` treated as defaults - No Mongoose migration needed: the `containers` field is a schemaless `Schema.Types.Array`; all validation happens at the Zod layer - Covered by tests: `useDashboardContainers.test.tsx` "legacy dashboard upgrade path" and `dashboardSections.test.tsx:273` "old dashboards with type field still parse" ### Review feedback addressed - ✅ Semantic tokens everywhere (replaced raw Mantine color tokens) - ✅ Mantine components over raw HTML / `style` props - ✅ Component decomposition — `GroupTabBar` extracted from the main component; `DashboardContainerRow` extracted from inline render in `DBDashboardPage` - ✅ Accessibility — chevron a11y, hidden controls removed from tab order - ✅ URL state for per-viewer UI (`activeTabId` + collapse/expand) — the only non-structural `setDashboard` call migrated off the server - ✅ Renamed `GroupContainer` → `DashboardContainer` to match the schema type - ✅ Auto-delete emptied source containers after Cmd+G regrouping - ✅ 3-option prompt for group delete (Cancel / Ungroup Tiles / Delete Group & Tiles) - ✅ Alert dot now appears on expanded plain/single-tab group headers (previously only on collapsed) - ✅ Fix: spurious `setDashboard` on every tile drag in multi-tab containers (layout handler was built with all-tab tiles but RGL only renders visible-tab tiles) - ✅ Take viewer to newly-created tab after Add Tab (clears stale URL override) - ✅ Removed redundant WHAT-style comments; kept WHY / invariant comments - ✅ Removed dead API surface (unused `handleToggleCollapsed` export from the hook) - ✅ Documented the `:` separator invariant in the `activeTabs` URL param ## Test plan - [x] 1515 app unit tests pass - [x] 745 common-utils unit tests pass - [x] ESLint clean across all packages (`make ci-lint`) - [x] TypeScript clean (app, common-utils, api) - [x] Changeset included - [x] Verified on Vercel preview Builds on #1972
…ered options (hyperdxio#2015) ## Summary Merges the separate "section" and "group" container types into a single **DashboardContainer** concept with configurable options: - **Collapsible** (optional, default true) — chevron toggle with URL-based collapse state - **Bordered** (optional, default true) — visual border around the container - **Tabbed** (optional) — add tabs to any container; tab bar appears with 2+ tabs. Active tab is per-viewer via URL state. - **Alert indicators** — red dot on tabs (multi-tab groups) and on the group header (plain/collapsed) when a tile is in `AlertState.ALERT`, so an alerting group is visible at a glance even when the alerting tile is below the fold This addresses the UX concern from hyperdxio#1972 that "section" and "group" are near-synonyms that force users to choose between two similar concepts. ### Screenshots **Dashboard with groups and tabs (expanded):**  **Overflow menu (Add Tab, Collapse, Border, Delete):**  **Collapsed with pipe-separated tab names (Overview | Latency | Errors):**  ### Commits (review guide) | Area | What to review | |------|----------------| | **Schema** — `types.ts` | new optional fields (`tabs`, `collapsible`, `bordered`, `activeTabId`, `tabId`); `type` discriminator removed | | **DnD infrastructure** | `DashboardDndContext.tsx`, `DashboardDndComponents.tsx`: sortable wrappers, drag handle | | **`DashboardContainer`** | `DashboardContainer.tsx` + `GroupTabBar.tsx`: unified component with tabs, collapse, borders, alerts, a11y | | **Hooks** | `useDashboardContainers.tsx`: container/tab CRUD. `useTileSelection.ts`: multi-select + Cmd+G (auto-deletes emptied source containers) | | **Dashboard page** | `DBDashboardPage.tsx`: wiring, alert computation, DnD, tile positioning, URL-based active-tab + collapse state | | **Tests** | `DashboardContainer.test.tsx`, `useDashboardContainers.test.tsx`, `useTileSelection.test.tsx`, `dashboardSections.test.tsx` | ### Key decisions - **No `type` discriminator** — containers are defined by properties. Extensible via additional fields later (e.g., planned variable-repeat feature could add `repeat: { filterId; as: 'tabs' \| 'sections' }` cleanly onto the same container). - **Collapsed label** — pipe-separated tab names (max 4 + "…"), not dot-separated, no duplicate header. - **Alert indicators** — red dot on tabs with `AlertState.ALERT` tiles. Also shown at the group-header level when no tab bar is rendered (plain/collapsed), so a single alerting tile in a large dashboard isn't hidden. - **Accessibility** — chevron has role/tabIndex/aria-expanded/onKeyDown. Hidden controls removed from tab order. - **Per-viewer UI state via URL** — collapse/expand and active-tab are per-viewer (URL), falling back to DB defaults. Switching tabs in your browser doesn't affect another viewer's session. - **Tab delete confirmation** — deleting a tab with tiles opens a 3-button modal: Cancel / Move Tiles to [first remaining tab] / Delete Tab & Tiles. Destructive option is the red primary. - **Group delete confirmation** — deleting a group with tiles opens a 3-button modal: Cancel / Ungroup Tiles / Delete Group & Tiles. Ungroup preserves tiles as top-level; Delete removes them. Works consistently for plain groups, legacy sections, and multi-tab groups (all tiles across all tabs are handled). ### Backward compatibility - Dashboards without containers render as before - Old `type: 'section'` containers parse successfully (Zod strips extra field) - New optional fields (`collapsible`, `bordered`, `tabs`, `activeTabId`, `tabId`) — `undefined` treated as defaults - No Mongoose migration needed: the `containers` field is a schemaless `Schema.Types.Array`; all validation happens at the Zod layer - Covered by tests: `useDashboardContainers.test.tsx` "legacy dashboard upgrade path" and `dashboardSections.test.tsx:273` "old dashboards with type field still parse" ### Review feedback addressed - ✅ Semantic tokens everywhere (replaced raw Mantine color tokens) - ✅ Mantine components over raw HTML / `style` props - ✅ Component decomposition — `GroupTabBar` extracted from the main component; `DashboardContainerRow` extracted from inline render in `DBDashboardPage` - ✅ Accessibility — chevron a11y, hidden controls removed from tab order - ✅ URL state for per-viewer UI (`activeTabId` + collapse/expand) — the only non-structural `setDashboard` call migrated off the server - ✅ Renamed `GroupContainer` → `DashboardContainer` to match the schema type - ✅ Auto-delete emptied source containers after Cmd+G regrouping - ✅ 3-option prompt for group delete (Cancel / Ungroup Tiles / Delete Group & Tiles) - ✅ Alert dot now appears on expanded plain/single-tab group headers (previously only on collapsed) - ✅ Fix: spurious `setDashboard` on every tile drag in multi-tab containers (layout handler was built with all-tab tiles but RGL only renders visible-tab tiles) - ✅ Take viewer to newly-created tab after Add Tab (clears stale URL override) - ✅ Removed redundant WHAT-style comments; kept WHY / invariant comments - ✅ Removed dead API surface (unused `handleToggleCollapsed` export from the hook) - ✅ Documented the `:` separator invariant in the `activeTabs` URL param ## Test plan - [x] 1515 app unit tests pass - [x] 745 common-utils unit tests pass - [x] ESLint clean across all packages (`make ci-lint`) - [x] TypeScript clean (app, common-utils, api) - [x] Changeset included - [x] Verified on Vercel preview Builds on hyperdxio#1972
Summary
Dashboard organization feature: tiles can be organized into Groups — collapsible, optionally bordered containers with smart tab support, reordering via drag handles, and cross-container tile moves.
Concepts
Group — A container for tiles. Always has at least 1 tab internally. Configurable:
tabId.Container schema — No
typediscriminator. Containers are defined by properties (collapsible,bordered,tabs). Designed to be extensible via discriminated union if other container types are needed in the future (e.g. markdown blocks, alert rows, embeds).Features
+ Add→ "New Group"Implementation
DashboardContainerSchemaincommon-utils/types.tswithcollapsible,bordered,tabs(optional),activeTabIdGroupContainer— unified component (replaced separateSectionHeader)useDashboardContainers(container CRUD + tab lifecycle),useTileSelection(multi-select + Cmd+G)@dnd-kitfor container reordering, customDashboardDndContextPRs
References